﻿using System;
using System.Linq;
using System.ComponentModel;
using System.Collections.Generic;

namespace Sencha.Serialization
{
	public static class TypeConvert
	{
		static class BetweenTypes<FromType, ToType>
		{
			public readonly static TypeConverter Converter;
			public readonly static Func<FromType, ToType> ExplicitFrom;
			public readonly static Func<FromType, ToType> ImplicitFrom;
			public readonly static Func<ToType, FromType> ExplicitTo;
			public readonly static Func<ToType, FromType> ImplicitTo;
			public readonly static Func<ToType, FromType> ConvertibleTo;
			public readonly static Func<FromType, ToType> ConvertibleFrom;
			public readonly static Func<FromType, IFormatProvider, ToType> Transition;

			static BetweenTypes()
			{
				var sourceType = typeof(FromType);
				var resultType = typeof(ToType);

				var isResultNullableValueType = (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Nullable<>));
				var isSourceNullableValueType = (sourceType.IsGenericType && sourceType.GetGenericTypeDefinition() == typeof(Nullable<>));
				var isSourceIsObject = sourceType == typeof(Object);
				var isResultIsObject = resultType == typeof(Object);
				var isSourceIsEnum = sourceType.IsEnum;
				var isResultIsEnum = resultType.IsEnum;
				var isSourceIsString = sourceType == typeof(String);
				var isResultIsString = resultType == typeof(String);
				var methodVisibility = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static;

				var transitionMethod = default(System.Reflection.MethodInfo);

				if (isResultNullableValueType && isSourceNullableValueType)
					transitionMethod = typeof(TypeConvert).GetMethod("ConvertFromNullableToNullable", methodVisibility).MakeGenericMethod(Nullable.GetUnderlyingType(resultType), Nullable.GetUnderlyingType(sourceType));
				else if (isResultNullableValueType)
					transitionMethod = typeof(TypeConvert).GetMethod("ConvertToNullable", methodVisibility).MakeGenericMethod(Nullable.GetUnderlyingType(resultType), sourceType);
				else if (isSourceNullableValueType)
					transitionMethod = typeof(TypeConvert).GetMethod("ConvertFromNullable", methodVisibility).MakeGenericMethod(resultType, Nullable.GetUnderlyingType(sourceType));
				else if (isResultIsObject)
					transitionMethod = typeof(TypeConvert).GetMethod("ConvertToObject", methodVisibility).MakeGenericMethod(resultType, sourceType);
				else if (isSourceIsObject)
					transitionMethod = typeof(TypeConvert).GetMethod("ConvertFromObject", methodVisibility).MakeGenericMethod(resultType, sourceType);
				else if (isSourceIsEnum && isResultIsEnum)
					transitionMethod = ConvertMethodInfo.MakeGenericMethod(Enum.GetUnderlyingType(sourceType), Enum.GetUnderlyingType(resultType));
				else if (isSourceIsEnum && !isResultIsString)
					transitionMethod = ConvertMethodInfo.MakeGenericMethod(Enum.GetUnderlyingType(sourceType), resultType);
				else if (isResultIsEnum && !isSourceIsString)
					transitionMethod = ConvertMethodInfo.MakeGenericMethod(sourceType, Enum.GetUnderlyingType(resultType));

				if (transitionMethod != null)
				{
					Transition = (Func<FromType, IFormatProvider, ToType>)Delegate.CreateDelegate(typeof(Func<FromType, IFormatProvider, ToType>), transitionMethod);
					return;
				}

				foreach (var method in resultType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
				{
					if (method.IsSpecialName && method.Name == "op_Explicit" && method.ReturnType == sourceType && HasSingleParameterOfType(method, resultType))
						ExplicitTo = (Func<ToType, FromType>)Delegate.CreateDelegate(typeof(Func<ToType, FromType>), method, true);
					else if (method.IsStatic && method.IsSpecialName && method.Name == "op_Implicit" && method.ReturnType == sourceType && HasSingleParameterOfType(method, resultType))
						ImplicitTo = (Func<ToType, FromType>)Delegate.CreateDelegate(typeof(Func<ToType, FromType>), method, true);
					else if (method.IsSpecialName && method.Name == "op_Explicit" && method.ReturnType == resultType && HasSingleParameterOfType(method, sourceType))
						ExplicitFrom = (Func<FromType, ToType>)Delegate.CreateDelegate(typeof(Func<FromType, ToType>), method, true);
					else if (method.IsSpecialName && method.Name == "op_Implicit" && method.ReturnType == resultType && HasSingleParameterOfType(method, sourceType))
						ImplicitFrom = (Func<FromType, ToType>)Delegate.CreateDelegate(typeof(Func<FromType, ToType>), method, true);
				}
				foreach (var method in typeof(Convert).GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
				{
					if (method.Name.StartsWith("To") && method.ReturnType == sourceType && HasSingleParameterOfType(method, resultType))
						ConvertibleTo = (Func<ToType, FromType>)Delegate.CreateDelegate(typeof(Func<ToType, FromType>), method, true);
					else if (method.Name.StartsWith("To") && method.ReturnType == resultType && HasSingleParameterOfType(method, sourceType))
						ConvertibleFrom = (Func<FromType, ToType>)Delegate.CreateDelegate(typeof(Func<FromType, ToType>), method, true);
				}

				Converter = TypeDescriptor.GetConverter(sourceType);
				if (Converter != null && Converter.GetType() == typeof(TypeConverter))
					Converter = null;
			}

			private static bool HasSingleParameterOfType(System.Reflection.MethodInfo method, Type type)
			{
				var @params = method.GetParameters();
				return @params.Length == 1 && @params[0].ParameterType == type;
			}
		}
		static class TypeInfo<ToType>
		{
			// nullable
			public readonly static bool IsNullableValueType;
			public readonly static bool IsEnum;
			public readonly static bool IsString;
			public readonly static bool IsValueType;
			public readonly static bool IsFormattable;

			public readonly static TypeConverter Converter;
			public readonly static Func<string, ToType> ParseFn;
			public readonly static Func<string, IFormatProvider, ToType> ParseFormatedFn;
			public readonly static Func<ToType, string> ToStringFn;
			public readonly static Func<ToType, string, IFormatProvider, string> ToStringFormatedFn;

			static TypeInfo()
			{
				var resultT = typeof(ToType);

				IsValueType = resultT.IsValueType;

				if (resultT.IsGenericType && resultT.GetGenericTypeDefinition() == typeof(Nullable<>))
				{
					IsNullableValueType = true;
					return;
				}

				if (resultT.IsEnum)
				{
					Converter = new EnumConverter(resultT);
					IsEnum = true;
					return;
				}

				IsString = typeof(string) == resultT;
				IsFormattable = typeof(IFormattable).IsAssignableFrom(resultT);

				Converter = TypeDescriptor.GetConverter(typeof(ToType));
				if (Converter != null && Converter.GetType() == typeof(TypeConverter))
					Converter = null;

				foreach (var method in typeof(ToType).GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly))
				{
					if (method.IsStatic && method.Name == "Parse" && method.ReturnType == typeof(ToType))
					{
						var parseParams = method.GetParameters();
						if (parseParams.Length == 1 && parseParams[0].ParameterType == typeof(string))
							ParseFn = (Func<string, ToType>)Delegate.CreateDelegate(typeof(Func<string, ToType>), method, true);
						else if (parseParams.Length == 2 && parseParams[0].ParameterType == typeof(string) && parseParams[1].ParameterType == typeof(IFormatProvider))
							ParseFormatedFn = (Func<string, IFormatProvider, ToType>)Delegate.CreateDelegate(typeof(Func<string, IFormatProvider, ToType>), method, true);
					}
					else if (!method.IsStatic && method.Name == "ToString" && method.ReturnType == typeof(String))
					{
						var toStringParams = method.GetParameters();
						if (toStringParams.Length == 0)
							ToStringFn = (Func<ToType, string>)Delegate.CreateDelegate(typeof(Func<ToType, string>), null, method, false);
						else if (toStringParams.Length == 2 && toStringParams[0].ParameterType == typeof(string) && toStringParams[1].ParameterType == typeof(IFormatProvider))
							ToStringFormatedFn = (Func<ToType, string, IFormatProvider, string>)Delegate.CreateDelegate(typeof(Func<ToType, string, IFormatProvider, string>), null, method, false);
					}
				}
			}
		}
		struct CacheKey
		{
			public readonly int T1;
			public readonly int T2;

			public CacheKey(int t1, int t2)
			{
				this.T1 = t1;
				this.T2 = t2;
			}

			public override int GetHashCode()
			{
				return T1 ^ T2;
			}

			public override bool Equals(object obj)
			{
				if (!(obj is CacheKey))
					return false;

				var other = (CacheKey)obj;
				return this.T1 == other.T1 && this.T2 == other.T2;
			}

			public override string ToString()
			{
				return this.GetHashCode().ToString();
			}
		}
		class CacheKeyEqualityComparer : IEqualityComparer<CacheKey>
		{
			public static IEqualityComparer<CacheKey> Instance = new CacheKeyEqualityComparer();
			#region IEqualityComparer<TypeKey> Members

			public bool Equals(CacheKey x, CacheKey y)
			{
				return x.T1 == y.T1 && x.T2 == y.T2;
			}

			public int GetHashCode(CacheKey obj)
			{
				return obj.T1 ^ obj.T2;
			}

			#endregion
		}

		private static readonly Dictionary<CacheKey, Delegate> s_CachedDelegates;
		public static readonly System.Reflection.MethodInfo ConvertMethodInfo = typeof(TypeConvert).GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static).Single(m => m.Name == "Convert" && m.IsGenericMethod);

		static TypeConvert()
		{
			s_CachedDelegates = new Dictionary<CacheKey, Delegate>(100, CacheKeyEqualityComparer.Instance);
		}

		public static ToType Convert<FromType, ToType>(FromType source, IFormatProvider format)
		{
			if (BetweenTypes<FromType, ToType>.Transition != null)
				return BetweenTypes<FromType, ToType>.Transition(source, format);

			return InternalConvert<FromType, ToType>(source, format);
		}
		public static object Convert(Type toType, object value, Type fromType, IFormatProvider format)
		{
			if (toType == null)
				throw new ArgumentNullException("toType");
			if (fromType == null)
				throw new ArgumentNullException("fromType");

			var dlg = default(Delegate);
			var cacheKey = new CacheKey
			(
				fromType.GetHashCode(), // it's not hashcode, it's an unique sync-lock number of type-object
				toType.GetHashCode()
			);

			lock (s_CachedDelegates)
			{
				if (!s_CachedDelegates.TryGetValue(cacheKey, out dlg))
				{
					var dlgType = typeof(Func<,,>).MakeGenericType(fromType, typeof(IFormatProvider), toType);
					var convMethod = ConvertMethodInfo.MakeGenericMethod(fromType, toType);
					s_CachedDelegates.Add(cacheKey, dlg = Delegate.CreateDelegate(dlgType, convMethod));
				}
			}

			try
			{
				return dlg.DynamicInvoke(new object[] { value, format });
			}
			catch (System.Reflection.TargetInvocationException te)
			{
				ReflectionExtentions.TryToPreserveStackTrace(te.InnerException);
				throw te.InnerException;
			}
		}

		private static ToType InternalConvert<FromType, ToType>(FromType source, IFormatProvider format)
		{
#if STRICT
			format = System.Globalization.CultureInfo.InvariantCulture;
#else
			if (format == null)
				format = System.Globalization.CultureInfo.InvariantCulture;
#endif
			var sourceObj = default(object);
			var sourceIsNullRef = default(bool);
			var sourceIsValueType = TypeInfo<FromType>.IsValueType;

			if (sourceIsValueType)
			{
				sourceIsNullRef = false;
				if (typeof(FromType) == typeof(ToType))
				{
#if NO_TYPE_REFS
					return (ResultT)sourceObj;
#else
					return __refvalue( __makeref(source),ToType);
#endif
				}
			}
			else if (!sourceIsValueType)
			{
				sourceObj = (object)source; // (1) box ref-type
				sourceIsNullRef = sourceObj == null;

				if (sourceObj is ToType)
					return (ToType)sourceObj;
				else if (sourceIsNullRef)
					return (ToType)(object)null;
			}

			// find explicit/implicit convertions between types
			if (BetweenTypes<FromType, ToType>.ImplicitFrom != null)
				return BetweenTypes<FromType, ToType>.ImplicitFrom(source);
			else if (BetweenTypes<ToType, FromType>.ImplicitTo != null)
				return BetweenTypes<ToType, FromType>.ImplicitTo(source);
			else if (BetweenTypes<FromType, ToType>.ExplicitFrom != null)
				return BetweenTypes<FromType, ToType>.ExplicitFrom(source);
			else if (BetweenTypes<ToType, FromType>.ExplicitTo != null)
				return BetweenTypes<ToType, FromType>.ExplicitTo(source);
			// try parse
			else if (TypeInfo<FromType>.IsString && TypeInfo<ToType>.ParseFormatedFn != null)
				return TypeInfo<ToType>.ParseFormatedFn((string)sourceObj, format);
			else if (TypeInfo<FromType>.IsString && TypeInfo<ToType>.ParseFn != null)
				return TypeInfo<ToType>.ParseFn((string)sourceObj);
			// try toString(formatted)
			else if (!sourceIsNullRef && TypeInfo<ToType>.IsString && TypeInfo<FromType>.ToStringFormatedFn != null)
				return (ToType)(object)TypeInfo<FromType>.ToStringFormatedFn(source, null, format);
			else if (!sourceIsNullRef && TypeInfo<ToType>.IsString && TypeInfo<FromType>.IsFormattable)
				return (ToType)(object)((sourceIsValueType ? (object)source : sourceObj) as IFormattable).ToString(null, format); // (2) box value-type
			// try convertible
			else if (BetweenTypes<ToType, FromType>.ConvertibleTo != null)
				return BetweenTypes<ToType, FromType>.ConvertibleTo(source);
			// try converter			
			else if (TypeInfo<ToType>.Converter != null && TypeInfo<ToType>.Converter.CanConvertFrom(typeof(FromType)))
				return (ToType)TypeInfo<ToType>.Converter.ConvertFrom(null, format as System.Globalization.CultureInfo, (sourceIsValueType ? (object)source : sourceObj)); // (3) box value-type
			else if (BetweenTypes<FromType, ToType>.Converter != null && BetweenTypes<FromType, ToType>.Converter.CanConvertTo(typeof(ToType)))
				return (ToType)BetweenTypes<FromType, ToType>.Converter.ConvertTo(null, format as System.Globalization.CultureInfo, (sourceIsValueType ? (object)source : sourceObj), typeof(ToType)); // (4) box value-type
			else if (!sourceIsNullRef && TypeInfo<ToType>.IsString && TypeInfo<FromType>.ToStringFn != null)
				return (ToType)(object)TypeInfo<FromType>.ToStringFn(source);
			// try ToString
			else if (!sourceIsNullRef && TypeInfo<ToType>.IsString)
				return (ToType)(object)(sourceIsValueType ? (object)source : sourceObj).ToString(); // (5) box value-type
			else
				throw new InvalidCastException(string.Format("Unable to convert value '{2}' of type '{1}' to requested type '{0}'", typeof(ToType), typeof(FromType), ((object)source) ?? "<null>")); // (6) box value-type/ref-type		
		}

		private static Nullable<ToType> ConvertFromNullableToNullable<ToType, FromType>(Nullable<FromType> value, IFormatProvider format)
			where FromType : struct
			where ToType : struct
		{
			if (value == null)
				return null;

			var result = TypeConvert.InternalConvert<FromType, ToType>(value.Value, format);
			return new Nullable<ToType>(result);
		}
		private static ToType ConvertFromNullable<ToType, FromType>(Nullable<FromType> value, IFormatProvider format)
			where FromType : struct
		{
			var result = TypeConvert.InternalConvert<FromType, ToType>(value.Value, format);
			return result;
		}
		private static Nullable<ToType> ConvertToNullable<ToType, FromType>(FromType value, IFormatProvider format)
			where ToType : struct
		{
			var result = TypeConvert.InternalConvert<FromType, ToType>(value, format);
			return new Nullable<ToType>(result);
		}
		private static ToType ConvertFromObject<ToType, FromT>(object value, IFormatProvider format)
		{
			if (value != null && value.GetType() == typeof(object))
				throw new InvalidCastException();

			if (value == null)
			{
				var fromType = typeof(ToType);
				if (fromType.IsValueType && (!fromType.IsGenericType || fromType.GetGenericTypeDefinition() != typeof(Nullable<>)))
					fromType = typeof(Nullable<>).MakeGenericType(fromType);

				return (ToType)TypeConvert.Convert(typeof(ToType), null, fromType, format);
			}

			return (ToType)TypeConvert.Convert(typeof(ToType), value, value.GetType(), format);
		}
		private static object ConvertToObject<ToType, FromT>(FromT value, IFormatProvider format)
		{
			return (object)value;
		}
	}
}
